﻿Imports System.Xml
Public Class CQLComparison
    Public Oper As String = ""
    Public OperParts As New List(Of Object)
End Class
Public Class CQLParser
    Private mFieldNameLower As Boolean = True
    Sub New(Optional ByVal fieldnamLower As Boolean = True)
        mFieldNameLower = fieldnamLower
    End Sub

    Private Enum BracketLocation
        StartLoc
        EndLoc
    End Enum
    Private Class SQLConcat
        Public Opers As List(Of String)
        Public Values As List(Of String)
    End Class
    Private Class BracketStartEndPosition
        Public StartIndex As Integer
        Public EndIndex As Integer
        Sub New(s As Integer, e As Integer)
            StartIndex = s
            EndIndex = e
        End Sub
    End Class
    Private _no As Integer = 0
    ' Private mSql As String = "cc > 5 or not a=b and not (dd=6) And ((((qq=5)))) and mm < 4 or ((cc>8 and cc< 9) and (dd>6) and not jj<5 and not (mm<4)) and jjj in (4,5,6,7)"
    'Public Function Parse(sql As String) As Boolean
    '    ' m_objDoc = New XmlDocument
    '    ' m_objRoot = m_objDoc.DocumentElement
    '    '优先级 () not and or
    '    Dim aoLast As AndOr = ParseAll()
    '    ' Dim filterXml As XmlElement = Nothing
    '    writeSQL(filterXml, aoLast)
    '    ' m_objDoc.AppendChild(filterXml)
    '    ' m_objDoc.Save("D:\Download\dltb\filter.xml")
    '    Return True
    'End Function
    ''' <summary>
    ''' 解析sql，并返回具有层级的sql语句
    ''' </summary>
    ''' <param name="sql"></param>
    ''' <returns>cql的sql层级</returns>
    Public Function Parse(sql As String) As CQLComparison
        _no = 0
        Dim aoLast As Object = Nothing
        Dim replacedBracket As New Dictionary(Of String, String)
        aoLast = parseConcatOne(sql, replacedBracket)
        If TypeOf aoLast Is CQLComparison Then
            ParseOperatorsAll(aoLast)
        Else
            aoLast = ParseOperator(aoLast)
        End If
        Return aoLast
    End Function
    ''' <summary>
    ''' 解析一个sql语句
    ''' 改函数会遍历执行，直到所有级别语句执行完成
    ''' </summary>
    ''' <param name="sql"></param>
    ''' <param name="replacedBracket"></param>
    ''' <returns></returns>
    Private Function parseConcatOne(sql As String,
                         replacedBracket As Dictionary(Of String, String)) As Object
        Dim bracketPeiDui As List(Of BracketStartEndPosition) = parseOutsideBracket(sql)
        Dim nValue As String = sql
        Dim paon As SQLConcat = Nothing
        If bracketPeiDui.Count > 0 Then
            nValue = replaceBracket(sql, bracketPeiDui, replacedBracket)
        End If
        paon = parseSQLConcat(nValue)
        '解析节点
        '优先级 () not and or
        If bracketPeiDui.Count > 0 OrElse paon.Opers.Count > 0 Then
            Dim aoLast As Object = Nothing
            Dim opers As List(Of String) = paon.Opers
            Dim operParts As List(Of String) = paon.Values
            For i As Integer = 0 To opers.Count - 1
                Dim left As Object = operParts.Item(i)
                Dim right As Object = operParts.Item(i + 1)
                Dim leftao As CQLComparison = parseNot(left)
                Dim rightao As CQLComparison = parseNot(right)
                If leftao IsNot Nothing Then
                    left = leftao
                End If
                If rightao IsNot Nothing Then
                    right = rightao
                End If
                Dim nAo As New CQLComparison
                If aoLast Is Nothing Then
                    aoLast = left
                End If
                nAo.OperParts.Add(aoLast)
                nAo.OperParts.Add(right)
                nAo.Oper = opers.Item(i)
                aoLast = nAo
            Next
            '没解析到括号，解析not
            If aoLast Is Nothing Then
                aoLast = parseNot(nValue)
            End If
            'not也没解析到，解析and or
            If aoLast Is Nothing Then
                Dim sqlq As String = getSQL(nValue, replacedBracket)
                aoLast = parseConcatOne(sqlq, replacedBracket)
            Else
                ParseConcatChild(aoLast, replacedBracket)
            End If
            Return aoLast
        Else
            Dim tao As CQLComparison = parseNot(sql)
            If tao IsNot Nothing Then
                Return tao
            Else
                Return sql
            End If
        End If
    End Function
    ''' <summary>
    ''' 根据原来替换的字符串，找到最原始的sql语句，再此进行下一步解析
    ''' </summary>
    ''' <param name="value"></param>
    ''' <param name="replacedBracket"></param>
    ''' <returns>实际的sql字符串</returns>
    Private Function getSQL(value As String,
                            replacedBracket As Dictionary(Of String, String)) As String
        Dim keys As List(Of String) = replacedBracket.Keys.ToList
        For Each key In keys
            Dim pos As Integer = value.IndexOf(key, StringComparison.CurrentCultureIgnoreCase)
            If pos >= 0 Then
                value = value.Replace(key, replacedBracket.Item(key))
                Return value
            End If
        Next
        Return value
    End Function
    ''' <summary>
    ''' 遍历解析
    ''' </summary>
    ''' <param name="aoLast"></param>
    ''' <param name="replacedBracket"></param>
    Private Sub ParseConcatChild(aoLast As CQLComparison, replacedBracket As Dictionary(Of String, String))
        Dim aoParts As List(Of Object) = aoLast.OperParts
        For i As Integer = 0 To aoParts.Count - 1
            Dim ao As Object = aoParts.Item(i)
            If TypeOf ao Is CQLComparison Then
                ParseConcatChild(ao, replacedBracket)
            Else
                Dim sql As String = getSQL(ao.ToString, replacedBracket)
                Dim subao As Object = parseConcatOne(sql, replacedBracket)
                aoParts.Item(i) = subao
            End If
        Next
    End Sub

    'Private Sub writeSQL(ByRef parentEle As XmlElement,
    '                          aoLast As AndOr)
    '    '  Dim node As XmlElement = CreateElement(aoLast.Oper)
    '    If parentEle Is Nothing Then
    '        parentEle = CreateElement(aoLast.Oper)
    '    End If
    '    Dim aoParts As List(Of Object) = aoLast.OperParts
    '    For Each ao In aoParts
    '        If TypeOf ao Is AndOr Then
    '            Dim partEle As XmlElement = CreateElement(CType(ao, AndOr).Oper)
    '            writeSQL(partEle, ao)
    '            parentEle.AppendChild(partEle)
    '        Else
    '            parentEle.InnerText = ao.ToString
    '            'partEle = CreateElement("Part")
    '            'partEle.InnerText = ao.ToString
    '        End If
    '        'node.AppendChild(partEle)
    '    Next

    'End Sub
    ''' <summary>
    ''' 将括号及内部内容替换为字符串，方便无括号解析，括号内部的括号会遍历解析，因此括号只提取最外层，
    ''' 一层一层剥离
    ''' </summary>
    ''' <param name="value"></param>
    ''' <param name="lstPeiDui"></param>
    ''' <param name="replaced"></param>
    ''' <returns></returns>
    Private Function replaceBracket(value As String,
                                    lstPeiDui As List(Of BracketStartEndPosition),
                                    ByVal replaced As Dictionary(Of String, String)) As String
        If lstPeiDui.Count = 0 Then
            Return value
        End If
        Dim nValue As String = value
        Dim level1 As Integer = 1
        Dim sIndex As Integer = 0
        For i As Integer = lstPeiDui.Count - 1 To 0 Step -1
            _no += 1
            Dim pd As BracketStartEndPosition = lstPeiDui.Item(i)
            Dim rl As String = "{" + _no.ToString.PadLeft(5, "0")
            Dim rlStr As String = nValue.Substring(pd.StartIndex + 1, pd.EndIndex - pd.StartIndex - 1)
            replaced.Add(rl, rlStr)
            nValue = nValue.Substring(0, pd.StartIndex) + rl + nValue.Substring(pd.EndIndex + 1)
        Next
        Return nValue
    End Function
    ''' <summary>
    ''' 解析字符串最外层括号（）
    ''' </summary>
    ''' <param name="value"></param>
    ''' <returns>最外层括号对列表</returns>
    Private Function parseOutsideBracket(value As String) As List(Of BracketStartEndPosition)
        Dim len As Integer = value.Length
        Dim d As New Dictionary(Of Integer, BracketLocation)
        Dim indexs As New List(Of Integer)
        For i As Integer = 0 To len - 1
            Dim substr As String = value.Substring(i, 1)
            If substr = "(" Then
                d.Add(i, BracketLocation.StartLoc)
                indexs.Add(i)
            ElseIf substr = ")" Then
                d.Add(i, BracketLocation.EndLoc)
                indexs.Add(i)
            End If
        Next

        Dim pds As New List(Of BracketStartEndPosition)
        For i As Integer = 0 To indexs.Count - 1
            Dim sCount As Integer = 0
            Dim eCount As Integer = 0
            Dim pos As Integer = indexs.Item(i) '
            '
            If d.Item(pos) = BracketLocation.StartLoc Then
                sCount += 1
            ElseIf d.Item(pos) = BracketLocation.EndLoc Then
                eCount += 1
            End If
            For j As Integer = i + 1 To indexs.Count - 1
                Dim pos1 As Integer = indexs.Item(j) '
                '
                If d.Item(pos1) = BracketLocation.StartLoc Then
                    sCount += 1
                ElseIf d.Item(pos1) = BracketLocation.EndLoc Then
                    eCount += 1
                End If
                If sCount = eCount Then
                    pds.Add(New BracketStartEndPosition(pos, pos1))
                    i = j
                    Exit For
                End If
            Next
        Next

        Return pds
    End Function
    'Private Function parseKH(value As String) As Dictionary(Of Integer, List(Of PeiDui))
    '    Dim len As Integer = value.Length
    '    Dim d As New Dictionary(Of Integer, BracketDui)
    '    Dim indexs As New List(Of Integer)
    '    For i As Integer = 0 To len - 1
    '        Dim substr As String = value.Substring(i, 1)
    '        If substr = "(" Then
    '            d.Add(i, BracketDui.StartDui)
    '            indexs.Add(i)
    '        ElseIf substr = ")" Then
    '            d.Add(i, BracketDui.EndDui)
    '            indexs.Add(i)
    '        End If
    '    Next
    '    'Dim pd As New List(Of String)
    '    Dim levels As New Dictionary(Of Integer, List(Of PeiDui))
    '    Dim level As Integer = 0
    '    Dim finded As New List(Of Integer)
    '    While (True)
    '        If finded.Count = indexs.Count Then
    '            Exit While
    '        End If
    '        level += 1
    '        levels.Add(level, New List(Of PeiDui))
    '        For i As Integer = 0 To indexs.Count - 1
    '            Dim sCount As Integer = 0
    '            Dim eCount As Integer = 0
    '            Dim pos As Integer = indexs.Item(i)
    '            If finded.Contains(pos) Then
    '                Continue For
    '            End If
    '            If d.Item(pos) = BracketDui.StartDui Then
    '                sCount += 1
    '            ElseIf d.Item(pos) = BracketDui.EndDui Then
    '                eCount += 1
    '            End If
    '            For j As Integer = i + 1 To indexs.Count - 1
    '                Dim pos1 As Integer = indexs.Item(j)
    '                If finded.Contains(pos1) Then
    '                    Continue For
    '                End If
    '                If d.Item(pos1) = BracketDui.StartDui Then
    '                    sCount += 1
    '                ElseIf d.Item(pos1) = BracketDui.EndDui Then
    '                    eCount += 1
    '                End If
    '                If sCount = eCount Then
    '                    levels.Item(level).Add(New PeiDui(pos, pos1))
    '                    finded.Add(pos)
    '                    finded.Add(pos1)
    '                    i = j
    '                    Exit For
    '                End If
    '            Next
    '        Next
    '    End While
    '    Return levels
    'End Function
    ''' <summary>
    ''' 解析not连接
    ''' </summary>
    ''' <param name="value"></param>
    ''' <returns></returns>
    Private Function parseNot(value As String) As CQLComparison
        Dim pos As Integer = value.IndexOf("not", StringComparison.CurrentCultureIgnoreCase)
        If pos >= 0 Then
            Dim ao As New CQLComparison
            ao.Oper = "Not"
            ao.OperParts.Add(value.Substring(pos + 3))
            Return ao
        End If
        Return Nothing
    End Function
    ''' <summary>
    ''' 解析and，or连接符
    ''' </summary>
    ''' <param name="value"></param>
    ''' <returns></returns>
    Private Function parseSQLConcat(value As String) As SQLConcat
        Dim sqc As New SQLConcat
        Dim startIndex As Integer = 0
        Dim posOfOper As New List(Of String)
        Dim indexs As New List(Of Integer)
        While (True)
            Dim pos1 As Integer = value.IndexOf("and", startIndex, StringComparison.CurrentCultureIgnoreCase)
            Dim pos3 As Integer = value.IndexOf("or", startIndex, StringComparison.CurrentCultureIgnoreCase)
            Dim poss As New List(Of Integer)
            If pos1 >= 0 Then
                poss.Add(pos1)
            End If
            If pos3 >= 0 Then
                poss.Add(pos3)
            End If
            If poss.Count = 0 Then
                Exit While
            End If
            poss.Sort()
            Dim pos As Integer = poss.Item(0)
            If pos = pos1 Then
                posOfOper.Add("And")
            ElseIf pos = pos3 Then
                posOfOper.Add("Or")
            End If
            indexs.Add(pos)
            startIndex = pos + 1
        End While
        '
        Dim operParts As New List(Of String)
        Dim cValue As String = value
        For i As Integer = indexs.Count - 1 To 0 Step -1
            Dim pos As Integer = indexs.Item(i)
            Dim operPart As String = cValue.Substring(pos)
            operParts.Add(operPart)
            cValue = cValue.Substring(0, pos)
        Next
        If Not String.IsNullOrWhiteSpace(cValue) Then
            operParts.Add(cValue)
        End If
        operParts.Reverse()
        For i As Integer = 0 To posOfOper.Count - 1
            operParts.Item(i + 1) = operParts.Item(i + 1).Substring(posOfOper.Item(i).Length)
        Next
        sqc.Opers = posOfOper
        sqc.Values = operParts
        Return sqc
    End Function
#Region "parse Operators"
    ''' <summary>
    ''' 对获取的cql层级的参数进行遍历解析
    ''' </summary>
    ''' <param name="aoLast"></param>
    Private Sub ParseOperatorsAll(aoLast As CQLComparison)
        Dim aoParts As List(Of Object) = aoLast.OperParts
        For i As Integer = 0 To aoParts.Count - 1
            Dim ao As Object = aoParts.Item(i)
            If TypeOf ao Is CQLComparison Then
                ParseOperatorsAll(ao)
            Else
                Dim subao As CQLComparison = ParseOperator(ao.ToString)
                aoParts.Item(i) = subao
            End If
        Next
    End Sub
    Private Function ParseOperator(value As String) As CQLComparison
        Dim ao As CQLComparison = Nothing
        Dim sqlOGCOpers As New Dictionary(Of String, String)
        sqlOGCOpers.Add("<>", "PropertyIsNotEqualTo")
        sqlOGCOpers.Add("<=", "PropertyIsLessThanOrEqualTo")
        sqlOGCOpers.Add(">=", "PropertyIsGreaterThanOrEqualTo")
        sqlOGCOpers.Add(">", "PropertyIsGreaterThan")
        sqlOGCOpers.Add("<", "PropertyIsLessThan")
        sqlOGCOpers.Add("=", "PropertyIsEqualTo")
        sqlOGCOpers.Add("like", "PropertyIsLike")
        For Each kvp In sqlOGCOpers
            ao = ParseOneCommonOper(value, kvp.Key, kvp.Value)
            If ao IsNot Nothing Then
                Return ao
            End If
        Next
        'PropertyIsNull单独处理
        ao = ParseOneOperOfNull(value)
        If ao IsNot Nothing Then
            Return ao
        End If
        'in 单独处理
        ao = ParseOneOperOfIn(value)
        If ao IsNot Nothing Then
            Return ao
        End If
        Return ao
    End Function
    Private Function ParseOneCommonOper(value As String, sqlOper As String, ogcOper As String) As CQLComparison
        Dim pos As Integer
        pos = value.IndexOf(sqlOper, StringComparison.CurrentCultureIgnoreCase)
        If pos < 0 Then
            Return Nothing
        End If
        Dim ao As New CQLComparison
        ao.Oper = ogcOper
        addPropertyValue(ao, value.Substring(0, pos).Trim, value.Substring(pos + sqlOper.Length).Trim)
        Return ao
    End Function
    Private Function ParseOneOperOfNull(value As String) As CQLComparison
        'PropertyIsNull单独处理
        Dim pos As Integer
        pos = value.IndexOf("is", StringComparison.CurrentCultureIgnoreCase)
        If pos < 0 Then
            Return Nothing
        End If
        Dim v As String = value.Substring(pos + 2).Trim.ToLower
        If v = "null" Then
            Dim ao As New CQLComparison
            ao.Oper = "PropertyIsNull"
            addPropertyValue(ao, value.Substring(0, pos).Trim)
            Return ao
        End If
        Return Nothing
    End Function
    Private Function ParseOneOperOfIn(value As String) As CQLComparison
        'PropertyIsNull单独处理
        Dim pos As Integer
        pos = value.IndexOf("in", StringComparison.CurrentCultureIgnoreCase)
        If pos < 0 Then
            Return Nothing
        End If
        Dim v As String = value.Substring(pos + 2).Trim.ToLower
        Dim vSplit() As String = v.Split(",".ToCharArray, StringSplitOptions.RemoveEmptyEntries)
        Dim vlen As Integer = vSplit.Length
        If vlen = 0 Then
            Return Nothing
        End If
        Dim ao As New CQLComparison
        Dim fld As String = value.Substring(0, pos).Trim
        If vlen = 1 Then
            ao.Oper = "PropertyIsEqualTo"
            addPropertyValue(ao, fld, vSplit(0).Trim)
        Else
            ao.Oper = "Or"
            For Each v In vSplit
                Dim subao As New CQLComparison
                subao.Oper = "PropertyIsEqualTo"
                addPropertyValue(subao, fld, v.Trim)
                ao.OperParts.Add(subao)
            Next
        End If
        Return ao
    End Function
    Private Sub addPropertyValue(ao As CQLComparison,
                                 propertyname As String,
                                 Optional ByVal v As String = "")
        Dim proAO As New CQLComparison
        proAO.Oper = "PropertyName"
        proAO.OperParts.Add(getValidFieldName(propertyname))
        ao.OperParts.Add(proAO)
        If ao.Oper.ToLower = "PropertyIsNull".ToLower Then
            Return
        End If
        If String.IsNullOrWhiteSpace(v) Then
            v = ""
        End If
        'If Not String.IsNullOrWhiteSpace(v) Then
        Dim vAo As New CQLComparison
        vAo.Oper = "Literal"
        vAo.OperParts.Add(getValidValue(v))
        ao.OperParts.Add(vAo)
        'End If
    End Sub
    Private Function getValidFieldName(name As String) As String
        If mFieldNameLower Then
            name = name.Trim.ToLower
        End If
        Return name.Replace("[", "").Replace("]", "").Replace("""", "")
    End Function
    Private Function getValidValue(v As String) As String
        Return v.Replace("""", "").Replace("'", "")
    End Function
#End Region
End Class
